/**
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.api.adwords.starterkit.utils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.rmi.RemoteException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.zip.GZIPInputStream;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.apache.log4j.Logger;
import org.json.JSONObject;

import com.google.api.adwords.lib.AdWordsUser;
import com.google.api.adwords.starterkit.settings.StarterKitSettings;
import com.google.api.adwords.v201101.cm.ApiException;
import com.google.api.adwords.v201101.cm.DateRange;
import com.google.api.adwords.v201101.cm.DownloadFormat;
import com.google.api.adwords.v201101.cm.Operator;
import com.google.api.adwords.v201101.cm.Predicate;
import com.google.api.adwords.v201101.cm.ReportDefinition;
import com.google.api.adwords.v201101.cm.ReportDefinitionDateRangeType;
import com.google.api.adwords.v201101.cm.ReportDefinitionField;
import com.google.api.adwords.v201101.cm.ReportDefinitionOperation;
import com.google.api.adwords.v201101.cm.ReportDefinitionReportType;
import com.google.api.adwords.v201101.cm.ReportDefinitionServiceInterface;
import com.google.api.adwords.v201101.cm.Selector;


/**
 * The class <code>AdWordsReportHandler</code> deals smoothly with 
 * huge adwords reports. 
 *
 * @author rodrigo.f.vale@gmail.com (Rodrigo Vale)
 */
public class AdWordsReportHandler {
  private static final Logger log = Logger.getLogger("StarterKit");
  private XMLInputFactory factory;
  
  private ServiceUtils serviceUtils;
  private StarterKitSettings starterKitSettings;
    
  XMLStreamReader streamReader;
  JSONObject jsonObject;
  HashMap<String,String> row = new HashMap<String,String>(); 


  /**
   * Instantiates a new adwords report handle.
   * 
   * @param serviceUtils The Starter Kit service utils object
   * @param starterKitSettings The Starter Kit settings object
   */
  public AdWordsReportHandler(ServiceUtils serviceUtils, 
      StarterKitSettings starterKitSettings) {
    this.serviceUtils = serviceUtils;
    this.starterKitSettings = starterKitSettings;
    
    factory = XMLInputFactory.newInstance();
    streamReader = null;
    jsonObject = new JSONObject();    
  }  

  /**
   * Sets the report.
   *
   * @param fileName the new report
   */
  private void setReport(String fileName) {
    try {
      //streamReader = factory.createXMLStreamReader(new FileReader(fileName));
      streamReader = factory.createXMLStreamReader(new FileInputStream(fileName), "UTF-8");
    } catch (FileNotFoundException e) {
      DumpExceptions.dumpIOException(e);
    } catch (XMLStreamException e) {
      DumpExceptions.dumpXMLStreamException(e);
    }
  }
  

  /**
   * Prepares the API calls to request the report and request it to the API
   * servers
   * 
   * @param reportType the adwords report type
   * @param fields the fields available in the report
   * @return the report ID 
   */
  protected long generateReportDefinition(
      ReportDefinitionReportType reportType, String[] fields, 
      Predicate[] predicates) {
    
    log.debug("Requesting the report");

    // Get the ReportDefinitionService.
    ReportDefinitionServiceInterface reportDefinitionService 
        = serviceUtils.getReportService();

    String[] validFields = removeInvalidFields(reportType, fields);

    // Create selector.
    Selector selector = new Selector();
    selector.setFields(validFields);
    selector.setDateRange(new DateRange(starterKitSettings.getReportStart(), 
        starterKitSettings.getReportEnd()));
    if (predicates != null) {
      selector.setPredicates(predicates);
    }
    // Create report definition.
    ReportDefinition reportDefinition = new ReportDefinition();
    reportDefinition.setReportName("StarterKit report - #" +
        System.currentTimeMillis());
    reportDefinition
        .setDateRangeType(ReportDefinitionDateRangeType.CUSTOM_DATE);
    reportDefinition.setReportType(reportType);
    reportDefinition.setDownloadFormat(DownloadFormat.GZIPPED_XML);
    reportDefinition.setSelector(selector);

    // Create operations.
    ReportDefinitionOperation operation = new ReportDefinitionOperation();
    operation.setOperand(reportDefinition);
    operation.setOperator(Operator.ADD);

    ReportDefinitionOperation[] operations = new ReportDefinitionOperation[] { 
        operation };

    // Add report definition.
    ReportDefinition[] result = null;
    int tries = 0;
    do {
      try {
        result = reportDefinitionService.mutate(operations);
        tries = 2;
      } catch (ApiException e) {
        DumpExceptions.dumpApiException(e); 
        log.debug("Will try again in 30s...");
        Utils.sleep(30000);
      } catch (RemoteException e) {
        log.debug(e.getMessage());
        log.debug("Will try again in 30s...");
        Utils.sleep(30000);
      }
    } while (tries < 2);
    return result[0].getId();
  }

  /**
   * Main method of this class. Request an AdWords report using the fields 
   * specified in the fields param and save the report in a local file
   * 
   * @param reportType the adwords report type
   * @param fields the fields available in the report
   * @param fileName the local file name the will contain the report content 
   */
  public void requestAdWordsReport(ReportDefinitionReportType reportType,
      String[] fields, String fileName, Predicate[] predicates) {
    long reportDefinitionId = generateReportDefinition(reportType, fields, 
        predicates);
    int tries = 0;
    do {
      try {
        downloadReport(reportDefinitionId, fileName);
        tries = 5;
      } catch (FileNotFoundException e) {
        log.error(e.getMessage());
        log.error("Can't move forward. Sorry!");
        System.exit(1);
      } catch (IOException e) {
        log.debug(e.getMessage());
        Utils.sleep(60000);
      }
    } while (tries < 5);

    setReport(fileName);
  }  
  
  /**
   * Downloads a report form AdWords defined by the reportId and save it 
   * in a file
   * 
   * @param reportDefinitionId the adwords report id
   * @param fileName the file to save the report 
   * @throws IOException if an I/O error occurs
   * @throws FileNotFoundException if an I/O error occurs
   */
  protected void downloadReport(long reportDefinitionId,
      String fileName) throws FileNotFoundException, IOException {
    
    log.debug("Downloading the report");
    
    String url = "https://adwords.google.com/api/adwords/reportdownload?__rd=" +
    reportDefinitionId;
    AdWordsUser user = serviceUtils.getAdWordsUser();

    HttpURLConnection urlConn = 
        (HttpURLConnection) new URL(url).openConnection();
    urlConn.setRequestMethod("GET");
    urlConn.setRequestProperty("Authorization", "GoogleLogin auth=" +
        user.getRegisteredAuthToken());

    if (user.getClientCustomerId() != null) {
      urlConn
          .setRequestProperty("clientCustomerId", user.getClientCustomerId());
    } else if (user.getClientEmail() != null) {
      urlConn.setRequestProperty("clientEmail", user.getClientEmail());
    } else {
      urlConn.setRequestProperty("clientEmail", user.getEmail());
    }

    urlConn.connect();
    writeStreamToStream(urlConn.getInputStream(), 
        new FileOutputStream(new File(fileName)));
  }  
  
  /**
   * Writes the contents of the {@link InputStream} inputStream to the
   * {@link OutputStream} outputStream.
   *
   * @param inputStream the {@code InputStream} to read from
   * @param outputStream the {@code OutputStream} to write to
   * @throws IOException if an I/O error occurs
   */
  private void writeStreamToStream(InputStream inputStream, 
      OutputStream outputStream)
  throws IOException {
    GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
    BufferedInputStream bis = new BufferedInputStream(gzipInputStream);
    BufferedOutputStream bos = new BufferedOutputStream(outputStream);

    try {
      int i = 0;
      while ((i = bis.read()) != -1) {
        bos.write(i);
      }
    } finally {
      if (bis != null) {
        bis.close();
      }
      if (bos != null) {
        bos.close();
      }
    }
  }  

  /**
   * Checks if there is a new line available on report
   *
   * @param tag the new line tag
   * @return true, if new line available
   */
  public boolean hasNext() {
    String tag = "row";
  row.clear();
    boolean inTag = false;
    try {
      while ((streamReader!=null) && (streamReader.hasNext())){
        streamReader.next();
        if(streamReader.getEventType() == XMLStreamReader.START_ELEMENT){
          if (streamReader.getLocalName().equals(tag)) {            
            inTag = true;
          }
          if (inTag ) {
            for (int i =0; i < streamReader.getAttributeCount(); i++) {
              String name = streamReader.getAttributeLocalName(i);
              String value = streamReader.getAttributeValue(i);
              row.put(name, value);
            }
          }
        }
        if(streamReader.getEventType() == XMLStreamReader.END_ELEMENT){
          if (streamReader.getLocalName().equals(tag)) {
            return true;
          }
        }
      }
    } catch (XMLStreamException e) {
      DumpExceptions.dumpXMLStreamException(e);
    }    
    return false;
  }

  /**
   * Get the next line avaialble on the report
   *
   * @return the hash map
   */
  public HashMap<String,String> next() {
    return row;
  }
  
  /**
   * Removes all requested fields that are not available in the report type
   * for this account 
   *
   * @return an array with valid fields only
   */
  protected String[] removeInvalidFields(ReportDefinitionReportType reportType, 
      String[] fields) {
    HashSet<String> validFields = getReportFields(reportType);
    HashSet<String> requestedFields 
        = new HashSet<String>(Arrays.asList(fields));
    
    Iterator<String> i = requestedFields.iterator();
    HashSet<String> toBeRemoved = new HashSet<String>();  
    while (i.hasNext()) {
      String field = i.next();
      if (!validFields.contains(field)) {
        toBeRemoved.add(field);
        log.warn("Dropping field from report: " + field);
      }
    }
    
    requestedFields.removeAll(toBeRemoved);
    
    // convert back to an array
    String[] finalFields = (String[]) requestedFields.toArray(
        new String[requestedFields.size()]);    
        
    return finalFields;
  }

  /**
   * Get the report fields available for a specific account
   *
   * @return a hash set with the fields
   */
  protected HashSet<String> getReportFields(
      ReportDefinitionReportType reportType) {
    HashSet<String> reportFields = new HashSet<String>(); 

    // Get the ReportDefinitionService.
    ReportDefinitionServiceInterface reportDefinitionService 
        = serviceUtils.getReportService();
    
    // Get report fields.
    ReportDefinitionField[] reportDefFields;
    try {
      reportDefFields = reportDefinitionService.getReportFields(reportType);

      for (ReportDefinitionField reportDefinitionField : reportDefFields) {
        reportFields.add(reportDefinitionField.getFieldName());
      }
    } catch (Exception e) {
      log.debug(e.getMessage());
    }  
    return reportFields;
  }
}
